#pragma once

#include <GL\glew.h>

#include <vector>
#include <algorithm>
#include <optional>

#include "pipeline_impl.h"
#include "cmdbuf_impl.h"

namespace blu
{
	namespace impl
	{
		class BindPipelineCommand
			:public GLCommand
		{
		private:
			std::reference_wrapper<Pipeline> _pipeline;
		public:
			void execute(ExecuteContext &ctxt)
			{
				ctxt.BindPipeline(_pipeline.get());
			}
		};

		class BindVertexBufferCommand
			:public GLCommand
		{
			::GLuint _hVBO;
			const void *_offset;
			::GLuint _binding;
		private:
			void _bindGLVBuffer(
				SingleInputConvention &inConv,
				SingleFetchConvention &fconv)
			{
				::glBindBuffer(GL_ARRAY_BUFFER, _hVBO);

				auto srcType = fconv.source_type();
				auto loc = fconv.location();
				auto stride = inConv.stride();
				auto compGLType = getComponentGLType(srcType);
				auto szComps = getComponentSize(srcType);

				if (is_int_types(srcType) && fconv.conversion() == integer_conversion::keep)
					::glVertexAttribIPointer(loc, szComps, compGLType, stride, _offset);
				else if (compGLType == GL_DOUBLE)
					::glVertexAttribLPointer(loc, szComps, GL_DOUBLE, stride, _offset);
				else
				{
					bool norm = is_int_types(srcType) && fconv.conversion() == integer_conversion::normalize;
					::glVertexAttribPointer(loc, szComps, compGLType, norm, stride, _offset);
				}
			}
		public:
			BindVertexBufferCommand(::GLuint hvbo, ::GLuint binding, const void *offset)
				:_hVBO(hvbo), _binding(binding), _offset(offset)
			{

			}
			void execute(ExecuteContext &ctxt) override
			{
				auto &pipl = ctxt.getPipeline();
				std::for_each(pipl.inputConvsBegin(), pipl.inputConvsEnd(),
					[&](SingleInputConvention &inConv)
				{
					if (inConv.binding() != _binding)
						return;
					std::for_each(pipl.feedConvsBegin(), pipl.feedConvsEnd(),
						[&](SingleFetchConvention &fconv)
					{
						if (fconv.binding() != _binding)
							return;

						ctxt.getGLContext().makeVAOIfNo();
						_bindGLVBuffer(inConv, fconv);
					});
				});
			}
		};

		class BindIndexBufferCommand
			:public GLCommand
		{
		private:
			::GLuint _hEBO;
		public:
			BindIndexBufferCommand(
				RecordingContext &reCtxt,
				::GLuint hebo, ::GLenum indextype, const void *offset)
				:_hEBO(hebo)
			{
				reCtxt.notifyBindingIndexBuffer(BoundedIndexBuffer(offset, indextype));
			}

			void execute(ExecuteContext &ctxt) override
			{
				::glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, _hEBO);
			}
		};

		void feedVertexBuffer(
			CmdBuffer &cmdbuf,
			FeedingPlace &fedplace,
			Buffer &vbuf,
			std::uint32_t vbufoffset)
		{
			auto &recCtxt = cmdbuf.getRecordingContext();
			auto cmd = std::make_unique<BindVertexBufferCommand>(
				vbuf.handle(), fedplace.binding(), reinterpret_cast<const void*>(vbufoffset));
			cmdbuf.push(std::move(cmd));
		}

		void feedIndexBuffer(
			CmdBuffer &cmdbuf,
			Buffer &ibuf,
			std::uint32_t ibufoffset,
			type_identity indextype_)
		{
			auto &recCtxt = cmdbuf.getRecordingContext();
			auto cmd = std::make_unique<BindIndexBufferCommand>(
				recCtxt, ibuf.handle(), impl::getComponentGLType(indextype_), reinterpret_cast<const void*>(ibufoffset));
			cmdbuf.push(std::move(cmd));
		}

		class DirectDrawCommand
			:public GLCommand
		{
		private:
			::GLenum _topo;
			::GLuint _firstVert;
			::GLuint _vertCount;
			::GLuint _firstInstance;
			::GLuint _instanceCount;
		public:
			DirectDrawCommand(
				::GLuint firstVert,
				::GLuint vertCount,
				::GLuint firstInstance,
				::GLuint instanceCount,
				RecordingContext &ctxt)
				:_firstVert(firstVert), _vertCount(vertCount),
				_firstInstance(firstInstance), _instanceCount(instanceCount)
			{
				auto &curPipeline = ctxt.getCurrentPipeline();
				if (!curPipeline)
					throw std::invalid_argument("You must bind a pipeline first to perform drawing.");
				_topo = curPipeline->get().getPrimTopo();
			}
			void execute(
				ExecuteContext &ctxt) override
			{
				auto pipl = ctxt.getPipeline();
				::glDrawArraysInstancedBaseInstance(
					_topo, _firstVert, _vertCount, _instanceCount, _firstInstance);
			}
		};

		class IndexedDrawCommand
			:public GLCommand
		{
		private:
			::GLenum _topo;
			::GLenum _indexType;
			::GLuint _vertCount;
			::GLuint _firstInstance;
			::GLuint _instanceCount;
			::GLuint _baseVertex;
			const void *_offset;
		public:
			IndexedDrawCommand(
				::GLuint vertCount,
				::GLuint baseVertex,
				::GLuint firstInstance,
				::GLuint instanceCount,
				RecordingContext &ctxt)
				:_vertCount(vertCount), _firstInstance(firstInstance),
				_instanceCount(instanceCount), _baseVertex(baseVertex)
			{
				auto &curPipeline = ctxt.getCurrentPipeline();
				if (!curPipeline)
					throw std::invalid_argument("You must bind a pipeline first to perform drawing.");
				_topo = curPipeline->get().getPrimTopo();
				auto &curIBuf = ctxt.getCurrentIndexBuffer();
				if (!curIBuf)
					throw std::invalid_argument("You must bind an index buffer first to perform indexed drawing.");
				_offset = curIBuf->offset();
				_indexType = curIBuf->indexType();
			}
			void execute(ExecuteContext &ctxt) override
			{
				::glDrawElementsInstancedBaseVertexBaseInstance(
					_topo, _vertCount, _indexType, _offset, _instanceCount, _firstInstance, _firstInstance);
			}
		};
	}
}